﻿#include "WidgetCutter.h"
#include <vtkCellData.h>
#include <vtkPointData.h>
#include <vtkDataSet.h>
#include <vtkOutlineFilter.h>
#include <vtkScalarBarRepresentation.h>
#include <vtkPolyDataWriter.h>
#include <vtkCutter.h>
#include <vtkActor.h>
#include <vtkPlane.h>
#include <vtkPlanes.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkLookupTable.h>
#include <vtkScalarBarWidget.h>
#include <vtkScalarBarActor.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkDoubleArray.h>

namespace pst
{

    WidgetCutter::WidgetCutter(QObject* parent)
        : QObject(parent)
        , m_inputData(vtkSmartPointer<vtkPolyData>::New())
        , m_inputDataOriginal(vtkSmartPointer<vtkPolyData>::New())
        , m_planeWidget(vtkSmartPointer<vtkPlaneWidget>::New())
        , m_plane(vtkSmartPointer<vtkPlane>::New())
        , m_boxWidget(vtkSmartPointer<vtkBoxWidget>::New())
        , m_planes(vtkSmartPointer<vtkPlanes>::New())
        , m_cutter(vtkSmartPointer<vtkCutter>::New())
        , m_selectedDataMapper(vtkSmartPointer<vtkPolyDataMapper>::New())
        , m_cutterDataActor(vtkSmartPointer<vtkActor>::New())
        , m_cutterWidgetCall(vtkSmartPointer<vtkWidgetCall>::New())
        , m_lookupTable(vtkSmartPointer<vtkLookupTable>::New())
        , m_scalarBarWidget(vtkSmartPointer<vtkScalarBarWidget>::New())
        , m_scalarBarActor(vtkSmartPointer<vtkScalarBarActor>::New())
        , m_dataOutlineActor(vtkSmartPointer<vtkActor>::New())
        , m_interactor(vtkSmartPointer<vtkRenderWindowInteractor>::New())
        , m_cutterDataToSave(vtkSmartPointer<vtkPolyData>::New())
        , m_currentWidgetType(WidgetType::PLANE)
        , m_currentAddedPointArrayIndex{ -1 }
        , m_currentAddedCellArrayIndex{ -1 }
        , m_isScalarAttributeSet{ false }
        , m_currentArrayRange{ 0,11 }
    {
        InitLookupTable();
        InitPipeLine();
    }

    void WidgetCutter::SetInteractor(vtkRenderWindowInteractor* iren)
    {
        m_planeWidget->SetInteractor(iren);
        m_boxWidget->SetInteractor(iren);
        m_scalarBarWidget->SetInteractor(iren);

        m_interactor = iren;
    }

    void WidgetCutter::SetInputData(vtkDataSet* dataSet)
    {
        m_inputData = dataSet;
        m_inputDataOriginal->DeepCopy(dataSet);
    }

    std::vector<WidgetCutter::ArrayInformation>
        WidgetCutter::GetPointsArrayIndexAndNames()
    {
        return GetPointsArrayIndexAndNames(m_inputDataOriginal);
    }

    std::vector<WidgetCutter::ArrayInformation>
        WidgetCutter::GetCellsArrayIndexAndNames()
    {
        return GetCellsArrayIndexAndNames(m_inputDataOriginal);
    }

    bool WidgetCutter::SetActiveScalarAttribute(int index,
        const StructureType type, const int compIndex)
    {
        auto polyData = vtkPointSet::SafeDownCast(m_inputData);
        //移除上次额外添加的数组
        polyData->GetPointData()->RemoveArray(m_currentAddedPointArrayIndex);
        polyData->GetCellData()->RemoveArray(m_currentAddedCellArrayIndex);

        switch (type)
        {
        case StructureType::POINTS:
        {
            if (index >= m_inputData->GetPointData()->GetNumberOfArrays() || index < -1)
            {
                std::cout << "Error! Index illegal!" << std::endl;
                return false;
            }

            auto activeScalarArray = m_inputData->GetPointData()->GetArray(index);
            const auto nComponents = activeScalarArray->GetNumberOfComponents();

            auto time1 = clock();
            //单组分array,忽略组分提取，直接设置
            if (nComponents == 1)
            {
                m_inputData->GetPointData()->SetActiveAttribute(
                    index, vtkDataSetAttributes::AttributeTypes::SCALARS);
                m_lookupTable->SetTableRange(activeScalarArray->GetRange());
                m_currentArrayRange[0] = activeScalarArray->GetRange()[0];
                m_currentArrayRange[1] = activeScalarArray->GetRange()[1];
            }
            else
            {
                if (compIndex < -1 || compIndex >= nComponents)
                {
                    std::cout << "Error! Extracting component index is " << compIndex
                        << ", current array component is " << nComponents << std::endl;
                    return false;
                }

                vtkNew<vtkDoubleArray> magArray;
                bool isSuccess = GetSingleComponent(activeScalarArray, compIndex, magArray);
                if (!isSuccess)
                {
                    std::cout << "Extract component failed!" << std::endl;
                    return false;
                }

                m_currentAddedPointArrayIndex = polyData->GetPointData()->AddArray(magArray);
                m_inputData->GetPointData()->SetActiveAttribute(
                    m_currentAddedPointArrayIndex, vtkDataSetAttributes::AttributeTypes::SCALARS);
                m_lookupTable->SetTableRange(magArray->GetRange());
                m_currentArrayRange[0] = magArray->GetRange()[0];
                m_currentArrayRange[1] = magArray->GetRange()[1];
            }
            auto time2 = clock();
            std::cout << "Add scalar time  = " << time2 - time1 << std::endl;
            m_selectedDataMapper->SetScalarModeToUsePointData();
            break;
        }

        case StructureType::CELLS:
        {
            if (index >= m_inputData->GetCellData()->GetNumberOfArrays() || index < 0)
            {
                std::cout << "Error! Index illegal!" << std::endl;
                return false;
            }


            auto activeScalarArray = m_inputData->GetCellData()->GetArray(index);
            const auto nComponents = activeScalarArray->GetNumberOfComponents();

            auto time1 = clock();
            //单组分array,忽略组分提取，直接设置
            if (nComponents == 1)
            {
                m_inputData->GetCellData()->SetActiveAttribute(
                    index, vtkDataSetAttributes::AttributeTypes::SCALARS);
                m_lookupTable->SetTableRange(activeScalarArray->GetRange());
                m_currentArrayRange[0] = activeScalarArray->GetRange()[0];
                m_currentArrayRange[1] = activeScalarArray->GetRange()[1];
            }
            else
            {
                if (compIndex < -1 || compIndex >= nComponents)
                {
                    std::cout << "Error! Extracting component index is " << compIndex
                        << ", current array component is " << nComponents << std::endl;
                    return false;
                }

                vtkNew<vtkDoubleArray> magArray;
                bool isSuccess = GetSingleComponent(activeScalarArray, compIndex, magArray);
                if (!isSuccess)
                {
                    std::cout << "Extract component failed!" << std::endl;
                    return false;
                }

                m_currentAddedCellArrayIndex = polyData->GetCellData()->AddArray(magArray);
                std::cout << "indexxxxx = " << m_currentAddedCellArrayIndex << std::endl;
                m_inputData->GetCellData()->SetActiveAttribute(
                    m_currentAddedCellArrayIndex, vtkDataSetAttributes::AttributeTypes::SCALARS);
                m_lookupTable->SetTableRange(magArray->GetRange());
                m_currentArrayRange[0] = magArray->GetRange()[0];
                m_currentArrayRange[1] = magArray->GetRange()[1];
            }
            auto time2 = clock();
            std::cout << "Add scalar time  = " << time2 - time1 << std::endl;
            m_selectedDataMapper->SetScalarModeToUseCellData();
            //        m_currentUsingScalarType = StructureType::CELLS;
            break;
        }

        default:
            std::cout << "Wrong structure type" << std::endl;
            return false;
            break;
        }
        m_isScalarAttributeSet = true;
        return true;
    }

    void WidgetCutter::SetWidgetType(WidgetType type)
    {
        m_currentWidgetType = type;
    }

    void WidgetCutter::SetPlaneNormalDirection(PlaneNormalDirection direction)
    {
        //PlaceWidget必须先于SetNormal调用，但似乎该函数并无效果。
    //    m_planeWidget->PlaceWidget(m_inputData->GetBounds());
        switch (direction)
        {
        case PlaneNormalDirection::X:
        {
            m_planeWidget->SetNormal(1, 0, 0);
            m_plane->SetNormal(1, 0, 0);
            break;
        }
        case  PlaneNormalDirection::Y:
        {
            m_planeWidget->SetNormal(0, 1, 0);
            m_plane->SetNormal(0, 1, 0);
            break;
        }
        case PlaneNormalDirection::Z:
        {
            m_planeWidget->SetNormal(0, 0, 1);
            m_plane->SetNormal(0, 0, 1);
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void WidgetCutter::SetRepresentationType(PlaneRepresentation type)
    {
        switch (type)
        {
            //    case  PlaneRepresentation::OUTLINE:
            //    {
            ////        m_planeWidget->SetRepresentationToOutline();
            //        m_cutterDataActor->GetProperty()->SetRepresentationToPoints();
            //        break;
            //    }
        case PlaneRepresentation::WIREFRAME:
        {
            //        m_planeWidget->SetRepresentationToWireframe();
            m_cutterDataActor->GetProperty()->SetRepresentationToWireframe();
            break;
        }
        case PlaneRepresentation::SURFACE:
        {
            //        m_planeWidget->SetRepresentationToSurface();
            m_cutterDataActor->GetProperty()->SetRepresentationToSurface();
            break;
        }
        case PlaneRepresentation::POINTS:
        {
            m_cutterDataActor->GetProperty()->SetRepresentationToPoints();
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void WidgetCutter::SetCenter(double x[3])
    {
        m_planeWidget->SetCenter(x);
        m_plane->SetOrigin(x);
    }

    void WidgetCutter::TurnAllWidgetOff()
    {
        if (m_planeWidget->GetInteractor())
        {
            m_planeWidget->Off();
        }
        if (m_boxWidget->GetInteractor())
        {
            m_boxWidget->Off();
        }
    }

    void WidgetCutter::TurnWidgetOn()
    {
        switch (m_currentWidgetType)
        {
        case WidgetType::BOX:
            m_boxWidget->On();
            break;
        case WidgetType::PLANE:
            m_planeWidget->On();
            break;
        default:
            break;
        }
    }

    void WidgetCutter::Update()
    {
        m_boxWidget->SetInputData(m_inputData);
        m_boxWidget->PlaceWidget(m_inputData->GetBounds());
        m_planes->SetBounds(m_inputData->GetBounds());

        //默认开启plane
        m_planeWidget->SetInputData(m_inputData);
        m_planeWidget->PlaceWidget(m_inputData->GetBounds());
        m_planeWidget->On();
        m_cutterWidgetCall->SetCallbackWidgetType(vtkWidgetCall::CallbackWidgetType::PLANE);

        m_cutter->SetCutFunction(m_plane);
        m_cutter->SetInputData(m_inputData);
        m_cutter->Update();

        m_selectedDataMapper->Update();

        m_scalarBarWidget->SetScalarBarActor(m_scalarBarActor);
        //    m_scalarBarWidget->SetResizable(true);

        vtkScalarBarRepresentation* rep =
            vtkScalarBarRepresentation::SafeDownCast(m_scalarBarWidget->GetRepresentation());
        rep->SetPosition2(0.05, 0.7);
        rep->SetPosition(0.01, 0.1);
        m_scalarBarWidget->On();

        GenerateDataOutlineBox();
    }

    void WidgetCutter::refreshWidget()
    {
        switch (m_currentWidgetType)
        {
            //将widget的interactor置空SetInteractor(NULL)
            //是为了能让每个widget都能响应键盘事件，否则其只响应最早设置interactor的widget
        case WidgetType::BOX:
        {
            m_planeWidget->Off();
            m_planeWidget->SetInteractor(nullptr);

            m_boxWidget->SetInteractor(m_interactor);
            m_boxWidget->On();

            //置空再设置是为了让interactor优先响应m_boxWidget键盘事件
            m_scalarBarWidget->SetInteractor(nullptr);
            m_scalarBarWidget->SetInteractor(m_interactor);
            m_scalarBarWidget->On();

            m_cutter->SetCutFunction(m_planes);
            m_cutterWidgetCall->SetCallbackWidgetType(vtkWidgetCall::CallbackWidgetType::BOX);
            m_cutterWidgetCall->Execute(m_boxWidget, 0, nullptr);
            break;
        }
        case WidgetType::PLANE:
        {
            m_boxWidget->Off();
            m_boxWidget->SetInteractor(nullptr);

            m_planeWidget->SetInteractor(m_interactor);
            m_planeWidget->On();

            //置空再设置是为了让interactor优先响应m_planeWidget键盘事件
            m_scalarBarWidget->SetInteractor(nullptr);
            m_scalarBarWidget->SetInteractor(m_interactor);
            m_scalarBarWidget->On();

            m_cutter->SetCutFunction(m_plane);
            m_cutterWidgetCall->SetCallbackWidgetType(vtkWidgetCall::CallbackWidgetType::PLANE);
            m_cutterWidgetCall->Execute(m_planeWidget, 0, nullptr);
            break;
        }
        default:
            break;
        }
    }

    vtkProperty* WidgetCutter::GetWidgetPlaneProperty()
    {
        return m_planeWidget->GetPlaneProperty();
    }

    vtkProperty* WidgetCutter::GetWidgetHandelProperty()
    {
        return m_planeWidget->GetHandleProperty();
    }

    vtkPlaneWidget* WidgetCutter::GetPlaneWidget()
    {
        return m_planeWidget;
    }

    vtkBoxWidget* WidgetCutter::GetBoxWidget()
    {
        return m_boxWidget;
    }

    vtkCutter* WidgetCutter::GetCutter()
    {
        return m_cutter;
    }

    vtkPolyDataMapper* WidgetCutter::GetMapper()
    {
        return m_selectedDataMapper;
    }

    vtkActor* WidgetCutter::GetCutterActor()
    {
        return m_cutterDataActor;
    }

    bool WidgetCutter::SetScalarBarRangeToAuto()
    {
        if (!m_isScalarAttributeSet)
        {
            std::cout << "Error, scalars attribute must be set !" << std::endl;
            return false;
        }
        m_lookupTable->SetTableRange(m_currentArrayRange[0], m_currentArrayRange[1]);
        return true;
    }

    void WidgetCutter::SetScalarBarRange(const double min, const double max)
    {
        m_lookupTable->SetTableRange(min, max);
    }

    void WidgetCutter::SetScalarBarTitle(const std::string& title)
    {
        m_scalarBarActor->SetTitle(title.c_str());
    }

    void WidgetCutter::SetScalarBarNumberOfLabel(const int num)
    {
        m_scalarBarActor->SetNumberOfLabels(num);
    }

    void WidgetCutter::SetScalarBarDisplayPosition(const int x, const int y)
    {
        m_scalarBarActor->SetDisplayPosition(x, y);
    }

    vtkScalarBarActor* WidgetCutter::GetScalarBar()
    {
        return m_scalarBarActor;
    }

    void WidgetCutter::SetDataOutlineActorColor(const double r, const double g, const double b)
    {
        m_dataOutlineActor->GetProperty()->SetColor(r, g, b);
    }

    vtkActor* WidgetCutter::GetDataOutlineActor()
    {
        return m_dataOutlineActor;
    }

    void WidgetCutter::WriteTheCutterData(const std::string& fileName)
    {
        vtkNew<vtkPolyDataWriter> writer;
        writer->SetInputData(m_cutterDataToSave);
        writer->SetFileName(fileName.c_str());
        try
        {
            writer->Update();
        }
        catch (const std::exception& e)
        {
            std::cout << e.what() << std::endl;
        }
    }

    void WidgetCutter::ResetCurrentWidgetPositon()
    {
        switch (m_currentWidgetType)
        {
        case WidgetType::BOX:
            m_boxWidget->PlaceWidget(m_inputData->GetBounds());
            break;
        case WidgetType::PLANE:
            m_planeWidget->SetCenter(m_inputData->GetCenter());
            m_planeWidget->PlaceWidget(m_inputData->GetBounds());
            break;
        default:
            break;
        }
    }

    vtkLookupTable* WidgetCutter::GetLookupTable()
    {
        return m_lookupTable;
    }

    void WidgetCutter::SetLookupTable(vtkLookupTable* lookupTable)
    {
        m_lookupTable = lookupTable;
    }

    void WidgetCutter::InitPipeLine()
    {
        m_planeWidget->SetResolution(10);
        m_planeWidget->SetHandleSize(0.001);
        m_planeWidget->SetPlaceFactor(0.1);
        m_planeWidget->GetPlaneProperty()->SetColor(.9, .4, .4);
        m_planeWidget->GetPlaneProperty()->SetPointSize(2);
        m_planeWidget->GetHandleProperty()->SetColor(0, .4, .7);
        m_planeWidget->GetHandleProperty()->SetLineWidth(1.5);
        m_planeWidget->SetRepresentationToOutline();
        m_planeWidget->SetPlaceFactor(1);

        m_boxWidget->SetPlaceFactor(1);
        m_boxWidget->SetHandleSize(0.003);
        m_boxWidget->SetPlaceFactor(1);

        m_cutterWidgetCall->m_planes = m_planes;



        m_scalarBarActor->SetTitle("Cutter");
        m_scalarBarActor->SetLookupTable(m_lookupTable);
        //    m_scalarBarActor->SetDisplayPosition(10,10);
        m_scalarBarActor->SetPosition(0.01, 0.1);
        m_scalarBarActor->SetPosition2(0.05, 0.7);
        m_cutterWidgetCall->m_singlePlane = m_plane;
        m_planeWidget->AddObserver(vtkCommand::InteractionEvent, m_cutterWidgetCall);
        m_boxWidget->AddObserver(vtkCommand::InteractionEvent, m_cutterWidgetCall);

        m_selectedDataMapper->SetInterpolateScalarsBeforeMapping(true);
        m_selectedDataMapper->SetColorModeToMapScalars();
        m_selectedDataMapper->ScalarVisibilityOn();
        m_selectedDataMapper->SetUseLookupTableScalarRange(1);
        m_selectedDataMapper->SetInputConnection(m_cutter->GetOutputPort());
        m_selectedDataMapper->SetLookupTable(m_lookupTable);
        m_cutterDataActor->SetMapper(m_selectedDataMapper);
    }

    void WidgetCutter::InitLookupTable()
    {
        //色调范围从蓝色到红色
        m_lookupTable->SetNumberOfColors(600);
        m_lookupTable->SetHueRange(0.67, 0.0);
        m_lookupTable->Build();
    }

    void WidgetCutter::GenerateDataOutlineBox()
    {
        vtkNew<vtkOutlineFilter> outline;
        outline->SetInputData(m_inputData);
        vtkNew<vtkPolyDataMapper> outlineMapper;
        outlineMapper->SetInputConnection(outline->GetOutputPort());
        outlineMapper->Update();
        m_dataOutlineActor->SetMapper(outlineMapper);
    }

    bool
        WidgetCutter::GetSingleComponent(vtkDataArray* array,
            const int componentIndex, vtkDoubleArray* returnArray)
    {

        auto nComponents = array->GetNumberOfComponents();
        std::cout << " nComponents = " << nComponents << std::endl;

        //检查下标合法性，-1代表取magnitude
        if (componentIndex < -1 || componentIndex >= nComponents)
        {
            return false;
        }

        //取单个组分的属性的0组分，即返回自己
        if (componentIndex == 0 && nComponents == 1)
        {
            returnArray->DeepCopy(vtkDoubleArray::SafeDownCast(array));
            return true;
        }

        const auto nTuples = array->GetNumberOfTuples();
        std::cout << " nTuples = " << nTuples << std::endl;

        vtkNew<vtkDoubleArray> singleArray;
        singleArray->SetNumberOfComponents(1);
        singleArray->SetNumberOfTuples(nTuples);

        //取magnitude
        if (componentIndex == -1)
        {
            std::cout << "magnitude " << std::endl;
            singleArray->SetName("Magnitude");
            for (vtkIdType i = 0; i < nTuples; ++i)
            {
                double mag = 0;
                for (int j = 0; j < nComponents; ++j)
                {
                    double tmp = array->GetComponent(i, j);
                    mag += tmp * tmp;
                }
                mag = sqrt(mag);
                singleArray->InsertTuple1(i, mag);
            }
        }
        else //取现有的单个组件
        {
            std::cout << "extract comp =  " << componentIndex << std::endl;

            if (componentIndex == 0)
            {
                singleArray->SetName("X");
            }
            else if (componentIndex == 1)
            {
                singleArray->SetName("Y");
            }
            else if (componentIndex == 2)
            {
                singleArray->SetName("Z");
            }
            else
            {
                singleArray->SetName(std::to_string(componentIndex).c_str());
            }

            for (vtkIdType j = 0; j < nTuples; ++j)
            {
                singleArray->InsertTuple1(j, array->GetComponent(j, componentIndex));
            }
        }
        returnArray->DeepCopy(singleArray);
        return true;
    }

    void
        WidgetCutter::SelectFieldsToSaveForCutter(bool useOrigianl,
            const std::vector<ArrayInformation>& selectedPointsFiledId,
            const std::vector<ArrayInformation>& selectedCellsFiledId)
    {
        m_cutterDataToSave->DeepCopy(m_cutter->GetOutput());

        if (useOrigianl)
        {
            auto pointsInfoOriginal = GetPointsArrayIndexAndNames(m_inputDataOriginal);
            auto cellsInfoOriginal = GetCellsArrayIndexAndNames(m_inputDataOriginal);
            SelectArrayFromCutterData(pointsInfoOriginal, cellsInfoOriginal);
        }
        else
        {
            SelectArrayFromCutterData(selectedPointsFiledId, selectedCellsFiledId);
        }
    }

    std::vector<WidgetCutter::ArrayInformation>
        WidgetCutter::GetPointsArrayIndexAndNames(vtkDataSet* data)
    {
        std::vector<WidgetCutter::ArrayInformation>  arrayInfoVector;
        WidgetCutter::ArrayInformation tempInfo;

        for (int i = 0; i < data->GetPointData()->GetNumberOfArrays(); ++i)
        {
            tempInfo.arrayIndex = i;
            tempInfo.arrayName = data->GetPointData()->GetArrayName(i);
            tempInfo.arrayComponent = data->GetPointData()->GetArray(i)->GetNumberOfComponents();
            arrayInfoVector.push_back(tempInfo);
        }
        return arrayInfoVector;
    }

    std::vector<WidgetCutter::ArrayInformation>
        WidgetCutter::GetCellsArrayIndexAndNames(vtkDataSet* data)
    {
        std::vector<WidgetCutter::ArrayInformation>  arrayInfoVector;
        WidgetCutter::ArrayInformation tempInfo;

        for (int i = 0; i < data->GetCellData()->GetNumberOfArrays(); ++i)
        {
            tempInfo.arrayIndex = i;
            tempInfo.arrayName = data->GetCellData()->GetArrayName(i);
            tempInfo.arrayComponent = data->GetCellData()->GetArray(i)->GetNumberOfComponents();
            arrayInfoVector.push_back(tempInfo);
        }
        return arrayInfoVector;
    }

    void WidgetCutter::SelectArrayFromCutterData(
        const std::vector<ArrayInformation>& selectedPointsFiled,
        const std::vector<ArrayInformation>& selectedCellsFiled)
    {
        auto pointsInfoCutter = GetPointsArrayIndexAndNames(m_cutterDataToSave);
        auto cellsInfoCutter = GetCellsArrayIndexAndNames(m_cutterDataToSave);

        for (int i = 0; i < pointsInfoCutter.size(); ++i)
        {
            for (int j = 0; j < selectedPointsFiled.size(); ++j)
            {
                //在其中，需要保留
                if (pointsInfoCutter[i].arrayIndex == selectedPointsFiled[j].arrayIndex)
                {
                    break;
                }
                //没找到，不在其中，移除多余列
                if (j == selectedPointsFiled.size() - 1)
                {
                    m_cutterDataToSave->GetPointData()->RemoveArray(pointsInfoCutter[i].arrayIndex);
                }
            }
        }

        for (int i = 0; i < cellsInfoCutter.size(); ++i)
        {
            for (int j = 0; j < selectedCellsFiled.size(); ++j)
            {
                //在其中，需要保留
                if (cellsInfoCutter[i].arrayIndex == selectedCellsFiled[j].arrayIndex)
                {
                    break;
                }
                if (j == selectedCellsFiled.size() - 1)
                {
                    m_cutterDataToSave->GetCellData()->RemoveArray(cellsInfoCutter[i].arrayIndex);
                }
            }
        }
    }


}
